package net.ib.mtalk.network;

import java.net.InetSocketAddress;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import net.ib.mtalk.constant.NetworkConstant.Session;
import net.ib.mtalk.global.GlobalApplication;
import net.ib.mtalk.global.GlobalPreferences;
import net.ib.mtalk.network.pipeline.NetClientChannelHandler;
import net.ib.mtalk.network.pipeline.TcpClientChannelHandler;
import net.ib.mtalk.network.pipeline.TcpClientPipelineFactory;
import net.ib.mtalk.util.MTalkLogWriter;

import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;

/**
 * TCP/IP 네트워크 연결을 만들고, 정보를 보내고, 연결을 닫는 루틴을 지원한다.
 * 실질적으로 연결은 0개 혹은 1개로 유지된다.
 * @author Arngard
 *
 */
public class TcpClient {

	public static final int NONE = -1;

	protected static Lock netTaskLock = new ReentrantLock();

	protected static NetClientChannelHandler channelHandler;
	protected static ClientBootstrap bootstrap;

	/*	접속 설정 시작	*/

	/**
	 * @return 연결이 끊겼을 경우 다음 연결을 시도할 때, 대상이 될 도메인 주소
	 */
	public static String getDomain() {
		String dynamicDomain = GlobalPreferences.getDynamicDomain();
		if (dynamicDomain == null) {
			return Session.BASE_DOMAIN;
		}
		return dynamicDomain;
	}

	/**
	 * 이 함수의 호출만으로 접속 상태가 즉시 변하지는 않는다.
	 * 대상 서버를 바로 바꾸고 싶다면 {@link TcpClient#reconnect()}를 호출해야한다. 
	 * @param newDomain 새로 세팅할 도메인. 만일 null이라면 함수는 아무 동작도 하지 않을 것이다.
	 * @return 대상 서버 주소가 바뀌었으면 true. 이 때 문자열에 대해 deep equals를 수행한다.
	 */
	public static boolean setDomain(String newDomain) {
		if (newDomain == null)
			return false;

		String before = getDomain();	// 여기에서 before는 null이 아니다.
		GlobalPreferences.setDynamicDomain(newDomain);

		if (before.equals(newDomain)) {
			return false;
		}
		return true;
	}

	/**
	 * 도메인 세팅 정보를 기본값으로 되돌린다.
	 */
	public static void unsetDomain() {
		GlobalPreferences.setDynamicDomain(null);
	}

	/**
	 * @return 연결이 끊겼을 경우 다음 연결을 시도할 때, 대상이 될 포트.
	 */
	public static int getPort() {
		int dynamicPort = GlobalPreferences.getDynamicPort();
		if (dynamicPort == NONE) {
			return Session.BASE_PORT;
		}
		return dynamicPort;
	}

	/**
	 * 이 함수의 호출만으로 접속 상태가 즉시 변하지는 않는다.
	 * 대상 서버를 바로 바꾸고 싶다면 {@link TcpClient#reconnect()}를 호출해야한다. 
	 * @param newPort 새로 세팅할 포트. 유효한 값이어야 한다. 0보다 작다면 함수는 아무 동작도 하지 않을 것이다.
	 * @return 대상 서버 포트가 바뀌었으면 true.
	 */
	public static boolean setPort(int newPort) {
		if (newPort < 0)
			return false;

		int before = getPort();
		GlobalPreferences.setDynamicPort(newPort);

		if (before == newPort) {
			return false;
		}
		return true;
	}

	/**
	 * 포트 세팅 정보를 기본값으로 되돌린다.
	 */
	public static void unsetPort() {
		GlobalPreferences.setDynamicPort(NONE);
	}

	/*	접속 설정 끝	*/

	/*	연결확인 시작	*/

	/**
	 * 호출한 순간에 네트워크 연결이 수립되어 있는지 검사한다.
	 * 이후에 실제로 데이터를 보낼 때까지 연결되어 있음을 보장하지는 않는다.
	 * @return 데이터 전송이 가능하다고 판단되면 true
	 * @see {@link NetState#getNetState(android.content.Context)}: Recommended to use with this function.
	 */
	public static boolean isConnected() {
		if (!channelHandler.validateChannel()) {
			return false;
		}
		if (channelHandler == null) {
			return false;
		}
		return true;
	}

	/*	연결확인 끝	*/

	/*	연결, 재연결 시작	*/

	/**
	 * 새로 연결을 만든다. 만일 연결이 있었다면, 끊고 새로 만든다.<br>
	 * <br>
	 * 연결을 새로 수립하면 RequestId의 유니크 공간이 리셋되는 점에 주의.
	 * 이전 연결에서 보냈던 리퀘스트에 대한 정보는 다음 연결까지 유지되지 않고 버려진다.
	 * 일반적으로 UI 컨트롤에서 네트워크에 데이터를 전송할 때에는 {@link TcpClient#send(TcpEnvelope)}만 사용하면 된다.
	 */
	public static void reconnect() {
		reconnect(TcpNetworkQueue.getInstance(), Session.THREAD_WAIT_TIME);
	}

	/**
	 * 새로 연결을 만든다. 만일 연결이 있었다면, 끊고 새로 만든다.<br>
	 * <br>
	 * 연결을 새로 수립하면 RequestId의 유니크 공간이 리셋되는 점에 주의.
	 * 이전 연결에서 보냈던 리퀘스트에 대한 정보는 다음 연결까지 유지되지 않고 버려진다.
	 * 일반적으로 UI 컨트롤에서 네트워크에 데이터를 전송할 때에는 {@link TcpClient#send(TcpEnvelope)}만 사용하면 된다.
	 * @param queue 외부의 스레드 풀을 사용할 때, 스레드 풀에 연결되는 큐의 참조
	 */
	public static void reconnect(BlockingQueue<Runnable> queue, final long waitTime) {
		final long offerTime = android.os.SystemClock.elapsedRealtime();

		queue.offer(new Runnable() {
			@Override
			public void run() {
				long executeTime = android.os.SystemClock.elapsedRealtime();
				if (executeTime - offerTime > waitTime) {
					MTalkLogWriter.d("TcpClient.reconnect().run() - waiting timeout");
					return;
				}

				lockReconnect();
			}
		});
	}

	protected static void lockReconnect() {
		MTalkLogWriter.v("TcpClient"+".lockReconnect() - STT");

		ChannelFuture channelFuture;
		if(channelHandler == null || ! channelHandler.validateChannel()) {
			netTaskLock.lock();
			try{
				nonLockClose();
				channelFuture = nonLockConnect();
			}finally{
				netTaskLock.unlock();
			}

			if (channelFuture == null || !channelFuture.isSuccess()) {	// 연결 실패라면
				MTalkLogWriter.v("TcpClient"+".lockReconnect() - END with fail");
				return;	// 중단.
			}
		}

		MTalkLogWriter.v("TcpClient"+".lockReconnect() - END");
	}

	protected static ChannelFuture nonLockConnect() {
		try {

			// check route
			if (NetState.getNetState(GlobalApplication.getContext(), getDomain()) < 0) {
				MTalkLogWriter.d("TcpClient.nonLockConnect(): host lookup failed");
				NetFlowControl.resetSessionAddress();
				return null;
			}
	
			// ready bootstrap
			if(bootstrap == null || channelHandler == null) {
				ChannelFactory cFactory = new NioClientSocketChannelFactory(Executors.newCachedThreadPool(), Executors.newCachedThreadPool());
				bootstrap = new ClientBootstrap(cFactory);
				channelHandler = new TcpClientChannelHandler(bootstrap, TcpClient.class.getSimpleName());
				bootstrap.setPipeline(new TcpClientPipelineFactory(channelHandler).getPipeline());
	
				bootstrap.setOption("tcpNoDelay", ! Session.USE_NAGLE_ALG);
				bootstrap.setOption("keepAlive", true);
				bootstrap.setOption("connectTimeoutMillis", Session.TIMEOUT_MILLIS);
			}
	
			MTalkLogWriter.d("TcpClient.nonLockConnect(): Connetion Tried="+getDomain()+":"+getPort());
			ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(getDomain(), getPort()));
			channelFuture.awaitUninterruptibly(Session.TIMEOUT_MILLIS);
			if (!channelFuture.isSuccess()) {
				MTalkLogWriter.d("TcpClient.nonLockConnect(): Connetion Fail");
				if (channelFuture.getCause() != null) {
					channelFuture.getCause().printStackTrace();
				}
				bootstrap.releaseExternalResources();
				bootstrap = null;
				nonLockClose();
			} else {
				MTalkLogWriter.d("TcpClient.nonLockConnect(): Connetion Success");
			}
			return channelFuture;
		} catch (Exception e) {
			MTalkLogWriter.e(e);
			return null;
		}
	}

	/*	연결, 재연결 끝	*/

	/*	끊기 시작	*/

	/**
	 * 현재의 연결을 닫는다.
	 */
	public static void close() {
		close(TcpNetworkQueue.getInstance());
	}

	/**
	 * 현재의 연결을 닫는다.
	 * @param queue 외부의 스레드 풀을 사용할 때, 스레드 풀에 연결되는 큐의 참조
	 */
	public static void close(BlockingQueue<Runnable> queue) {
		//final long offerTime = android.os.SystemClock.elapsedRealtime();

		queue.offer(new Runnable() {
			@Override
			public void run() {
				//long executeTime = android.os.SystemClock.elapsedRealtime();
				//if (executeTime - offerTime > NetworkConstant.THREAD_WAIT_TIME)
				//	return;

				lockClose();
			}
		});
	}

	public static void lockClose() {
		netTaskLock.lock();
		try{
			nonLockClose();
		}finally{
			netTaskLock.unlock();
		}
	}

	protected static boolean nonLockClose() {
		MTalkLogWriter.v("TcpClient"+".nonLockClose() - STT");

		if (channelHandler != null) {
			channelHandler.closeChannel();
			channelHandler = null;
		}

		if (bootstrap != null) {
			bootstrap.releaseExternalResources();
			bootstrap = null;
		}

		MTalkLogWriter.v("TcpClient"+".nonLockClose() - END");
		return true;
	}

	/*	끊기 끝	*/

	/*	전송 시작	*/

	/**
	 * {@link #send(BlockingQueue, TcpEnvelope, long, boolean)
	 *  send(TcpNetworkQueue.getQueue(), tcpEnvelope, NetworkConstant.Session.THREAD_WAIT_TIME, true)}와 같다.
	 */
	public static void send(final TcpEnvelope tcpEnvelope) {
		send(TcpNetworkQueue.getInstance(), tcpEnvelope, Session.THREAD_WAIT_TIME, true);
	}

	/**
	 * {@link #send(BlockingQueue, TcpEnvelope, long, boolean)
	 *  send(TcpNetworkQueue.getQueue(), tcpEnvelope, waitTime, true)}와 같다.
	 */
	public static void send(final TcpEnvelope tcpEnvelope, boolean autoConnect) {
		send(TcpNetworkQueue.getInstance(), tcpEnvelope, Session.THREAD_WAIT_TIME, autoConnect);
	}

	/**
	 * 현재 활성화되어 있는 채널에 데이터를 보낸다.
	 * 만일 연결이 없으면 연결을 만들어서 보낸다. 연결이 있을 때 재연결을 시도하지는 않는다.<br>
	 * 에러가 발생하면 {@link NetCallback#netError() revelope.getCallback().netError()} 가 호출된다.
	 * @param queue 외부의 스레드 풀을 사용할 때, 스레드 풀에 연결되는 큐의 참조
	 * @param tcpEnvelope 보낼 객체
	 * @param autoConnect 연결이 되어 있지 않은 경우 자동 재접속 여부
	 */
	public static void send(BlockingQueue<Runnable> queue, final TcpEnvelope tcpEnvelope, final long waitTime, final boolean autoConnect) {
		final long offerTime = android.os.SystemClock.elapsedRealtime();

		NetFlowControl.tryGetAuth();
		boolean result = queue.offer(new Runnable() {
			@Override
			public void run() {
				long executeTime = android.os.SystemClock.elapsedRealtime();
				if (executeTime - offerTime > waitTime) {
					MTalkLogWriter.d("TcpClient.send().run() : waiting timeout. try tcp error callback.");
					tcpEnvelope.passError(tcpEnvelope);
					return;
				}

				lockSend(tcpEnvelope, autoConnect);
			}
		});
		if(result == false) {
			MTalkLogWriter.d("TcpClient.send() : queue offer fail. try tcp error callback.");
			tcpEnvelope.passError(tcpEnvelope);
		}
	}

	protected static long lockSend(TcpEnvelope tcpEnvelope, boolean autoConnect) {
		long reVal = -1;

		MTalkLogWriter.v("TcpClient.lockSend() - STT");

		ChannelFuture channelFuture;
		netTaskLock.lock();
		try{
			if(channelHandler == null || !channelHandler.validateChannel()) {
				if (!autoConnect) {
					MTalkLogWriter.v("TcpClient.lockSend() - END with no auto-connect. try tcp error callback");
					tcpEnvelope.passError(tcpEnvelope);
					return reVal;	// 중단.
				}
				nonLockClose();
				channelFuture = nonLockConnect();

				if (channelFuture == null || !channelFuture.isSuccess()) {	// 연결 실패라면
					MTalkLogWriter.v("TcpClient.lockSend() - END with auto-connect fail. try tcp error callback");
					tcpEnvelope.passError(tcpEnvelope);
					return reVal;	// 중단.
				}
			}
			if(channelHandler != null) {
				MTalkLogWriter.v("TcpClient.lockSend(): write req");
				reVal = channelHandler.writeChannel(tcpEnvelope);
			} else {
				// 이 시점에서 channelHandler가 null이라는 것은 연결이 되지 않았다는 뜻
				MTalkLogWriter.d("TcpClient.lockSend(): Connection was not established. try tcp error callback");
				tcpEnvelope.passError(tcpEnvelope);
			}
		}finally{
			netTaskLock.unlock();
		}

		MTalkLogWriter.v("TcpClient.lockSend() - END");

		return reVal;
	}

	/*	전송 끝	*/

	/*	상태 관리 시작	*/

	public static void expiredCollector() {
		netTaskLock.lock();
		try{
			if (channelHandler != null) {
				channelHandler.expiredCollector();
			}
		}finally{
			netTaskLock.unlock();
		}
	}

	/*	상태 관리 끝	*/

}
